package uk.ac.kcl.cerch.r4r;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * @author michaelb
 *
 */

public class Cerif4RefProcessor {
	/**
	 * List of source metadata URLs (typically Dublin Core
	 * datastreams in the <strong>oai_dc</strong> namespace. 
	 */
	private List<String> sourceurls;
	
	/**
	 * Absolute URL of the transformer stylesheet.
	 */
	private String styleurl;
	
	/**
	 * Map of XSLT transformation parameters.
	 */
	private Map<String,String> parameters = null;
	
	/**
	 * Factory object for obtaining new XML transformers.
	 */
	private TransformerFactory transformerfactory 
			= TransformerFactory.newInstance();
	
	/**
	 * Factory object for obtraining XMK document builders.
	 */
	private DocumentBuilderFactory builderfactory 
			= DocumentBuilderFactory.newInstance();
	
	/**
	 * Construct a new Cerif4RefProcessor object.
	 * 		
	 * @param sources List of source XML URLs.
	 * @param style	  XSLT stylesheet for transformation.
	 * @param params  XSLT stylesheet parameters.
	 */
	public Cerif4RefProcessor(List<String> sources, String style, Map<String,String> params) {
		sourceurls = sources;
		styleurl = style;
		parameters = params;
	}
	
	/**
	 * Method for obtaining new document builder objects.
	 * 
	 * @return Document Builder object.
	 */
	private DocumentBuilder getDocumentBuilder() {
		DocumentBuilder builder = null;
		try {
			builder = builderfactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		return builder;
	}
	
	/**
	 * Method for obtaining new XML transformers with appropriate
	 * output formatting.
	 * 
	 * @param stylesheet Optional XSLT style sheet to use for transformation.
	 * @return XML transformer object.
	 */
	private Transformer getFormattedTransformer(String stylesheet) {
        Transformer transformer = null;
		try {
			if (stylesheet == null)
				transformer = transformerfactory.newTransformer();
			else
				transformer = transformerfactory.newTransformer(
						new StreamSource(styleurl));
		} catch (TransformerConfigurationException e) {
			e.printStackTrace();
		}
        transformer.setOutputProperty(
                "{http://xml.apache.org/xslt}indent-amount", "4");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        return transformer;		
	}
	
	/**
	 * Method for obtaining a new XML document with the approprate
	 * implementation.
	 * 
	 * @return XML document object.
	 */
	private Document getNewDocument() {
        DocumentBuilder builder = getDocumentBuilder();
        DOMImplementation impl = builder.getDOMImplementation();
        return impl.createDocument(null, null, null);        
	}
	
	/**
	 * Obtain a list of XML document objects from list
	 * of source URLS.
	 * 
	 * @return XML document list.
	 * @throws IOException
	 * @throws SAXException
	 */
	private List<Document> getSourceDocuments() throws IOException, SAXException {
		List<Document> sources = new ArrayList<Document>();
        for (String url : sourceurls) { 
			sources.add(getDocumentBuilder().parse(url));
        }
        return sources;
	}
	
	/**
	 * Write the transformed Cerif4Ref XML to a given output
	 * stream.
	 * 
	 * @param out The output stream to write to.
	 * @throws ParserConfigurationException
	 * @throws TransformerException
	 * @throws DOMException
	 * @throws IOException
	 * @throws SAXException
	 */
	public void writeXML(OutputStream out) 
			throws ParserConfigurationException, TransformerException,
				DOMException, IOException, SAXException {
        // get document transformers for standard & XSLT output
        Transformer transformer = getFormattedTransformer(null);
        Transformer xtransformer = getFormattedTransformer(styleurl);
        for (String key: parameters.keySet())
        	xtransformer.setParameter(key, parameters.get(key));
        
        // create a new document containing the sources, with
        // an "oai" root node.
        Document doc = getNewDocument();
        Element oairoot = doc.createElement("oai");
        doc.appendChild(oairoot);        
        for (Document srcdoc: getSourceDocuments()) {
			Node root = doc.importNode(srcdoc.getDocumentElement(), true);
			oairoot.appendChild(root);        	
        }
        
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
		StreamResult xmlfile = new StreamResult(out);        
		transformer.transform(new DOMSource(doc), new StreamResult(bs));
		xtransformer.transform(
				new StreamSource(new StringReader(bs.toString())), xmlfile);		
	}
	
	/**
	 * 
	 * @param out Writes a Zip file to the given output stream containing:
	 *   <ol>
	 *   	<li> The source XML documents</li>
	 *   	<li> The intermediate combined XML document</li>
	 *   	<li> The transformed Cerif4Ref output</li>
	 *   </ol>				
	 * @throws IOException
	 * @throws ZipException
	 * @throws TransformerException
	 * @throws DOMException
	 * @throws SAXException
	 */
	public void writeZip(OutputStream out)
			throws IOException, ZipException, TransformerException, DOMException, SAXException {
		ZipOutputStream zipfile = new ZipOutputStream(out);			
		
        // document transformer
        Transformer transformer = getFormattedTransformer(null);
        Transformer xtransformer = getFormattedTransformer(styleurl);
        for (String key: parameters.keySet())
        	xtransformer.setParameter(key, parameters.get(key));
        
        // document builder
        Document doc = getNewDocument();
        Element oairoot = doc.createElement("oai");
        doc.appendChild(oairoot);
        int docnum = 1;
        HashMap<String,Integer> names = new HashMap<String,Integer>();
        for (Document srcdoc: getSourceDocuments()) {
        	String title = getDocumentTitle(srcdoc, 
        			String.format("source-%d", docnum), names);
			Node root = doc.importNode(srcdoc.getDocumentElement(), true);
			oairoot.appendChild(root);
			ByteArrayOutputStream docbs = new ByteArrayOutputStream();
			transformer.transform(new DOMSource(srcdoc), new StreamResult(docbs));			
			zipfile.putNextEntry(new ZipEntry(String.format("%s.xml", title)));
			zipfile.write(docbs.toByteArray(), 0, docbs.size());
			docnum++;
        }
		
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
		transformer.transform(new DOMSource(doc), new StreamResult(bs));
		zipfile.putNextEntry(new ZipEntry("combined.xml"));
		zipfile.write(bs.toByteArray(), 0, bs.size());
		
		ByteArrayOutputStream xbs = new ByteArrayOutputStream();
		xtransformer.transform(
				new StreamSource(new StringReader(bs.toString())), new StreamResult(xbs));
		zipfile.putNextEntry(new ZipEntry("output.xml"));
		zipfile.write(xbs.toByteArray(), 0, xbs.size());
		zipfile.close();
	}
	
	/**
	 * Get the best document title for use by each document in the
	 * zip output file.  Looks for a dc:title attribute.  If one is
	 * not found falls back on the given fallback string, trimmed to
	 * 30 chars.
	 * @param document The XML document to search.
	 * @param fallback A string to use when so dc:title node is found.
	 * @param names A HashMap of previously used names, to avoid collision.
	 * @return A string of max 30 chars.
	 */
	private String getDocumentTitle(Document document, String fallback, 
			HashMap<String,Integer> names) {
		NodeList nodes = document.getElementsByTagName("dc:title");
		if (nodes.getLength() < 1)
			return fallback;
		Node textnode = nodes.item(0).getFirstChild();
		if (textnode.getNodeType() != Node.TEXT_NODE)
			return fallback;
		String fulltext = textnode.getTextContent();
		if (fulltext.length() > 30)
			fulltext = fulltext.substring(0, 29);
		if (names.containsKey(fulltext)) {
			int count = names.get(fulltext);
			names.put(fulltext, count + 1);
			fulltext = String.format("%s~%d", fulltext, count);			
		} else {
			names.put(fulltext, 1);
		}					
		return fulltext;
	}
}
